﻿using System;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Security.Principal;
using log4net;

namespace Kaleida.ServiceMonitor.Model.Runtime
{
    public class NamedWindowsUser : IWindowsUser, IDisposable
    {
        private static readonly ILog log = LogManager.GetLogger(typeof (NamedWindowsUser));

        private readonly WindowsIdentity windowsIdentity;
        private readonly string domainName;
        private readonly string userName;

        public NamedWindowsUser(string domainName, string userName, string password)
        {
            this.domainName = domainName;
            this.userName = userName;

            windowsIdentity = CreateWindowsIdentity(userName, domainName, password);
        }

// ReSharper disable InconsistentNaming
        public enum SECURITY_IMPERSONATION_LEVEL
        {
            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3
        }

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern bool LogonUser(string lpszUsername,
                                            string lpszDomain,
                                            string lpszPassword,
                                            int dwLogonType,
                                            int dwLogonProvider,
                                            ref IntPtr phToken);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool DuplicateToken(IntPtr hToken,
                                                int impersonationLevel,
                                                ref IntPtr hNewToken);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public extern static bool CloseHandle(IntPtr handle);

        const int LOGON32_PROVIDER_DEFAULT = 0;
        const int LOGON32_LOGON_INTERACTIVE = 2;
// ReSharper restore InconsistentNaming

        private WindowsImpersonationContext impersonatedUser;

        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        public static WindowsIdentity CreateWindowsIdentity(string userName, string domainName, string password)
        {
            var existingTokenHandle = IntPtr.Zero;
            var duplicateTokenHandle = IntPtr.Zero;

            try
            {
                bool impersonated = LogonUser(userName, domainName, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref existingTokenHandle);

                if (!impersonated)
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    log.ErrorFormat("Error impersonating {0}\\{1} using LogonUser. Error code={2}", domainName, userName, errorCode);
                    throw new System.ComponentModel.Win32Exception(errorCode);
                }

                bool duplicated = DuplicateToken(existingTokenHandle, (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, ref duplicateTokenHandle);

                if (!duplicated)
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    log.ErrorFormat("Error duplicating user {0}\\{1} using DuplicateToken. Error code={2}", domainName, userName, errorCode);
                    CloseHandle(existingTokenHandle);
                    throw new System.ComponentModel.Win32Exception(errorCode);
                }
                return new WindowsIdentity(duplicateTokenHandle);
            }
            finally
            {
                if (existingTokenHandle != IntPtr.Zero)
                    CloseHandle(existingTokenHandle);

                if (duplicateTokenHandle != IntPtr.Zero)
                    CloseHandle(duplicateTokenHandle);
            }
        }

        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        public void Impersonate()
        {
            impersonatedUser = windowsIdentity.Impersonate();
        }

        public string DomainName
        {
            get { return domainName; }
        }

        public string UserName
        {
            get { return userName; }
        }

        public void Revert()
        {
            if (impersonatedUser != null)
                impersonatedUser.Undo();
        }

        public void Dispose()
        {
            if (windowsIdentity != null)
                windowsIdentity.Dispose();
        }
    }    
}
